package com.espirit.eap.conversion;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.dispatcher.Dispatcher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.espirit.eap.conversion.ConversionContext.PagePosition;
import com.espirit.eap.conversion.ConversionContext.TagPosition;
import com.espirit.eap.pagelayout.DetailLayout;
import com.espirit.eap.pagelayout.Element;
import com.espirit.eap.pagelayout.ElementService;
import com.espirit.eap.pagelayout.Layout;
import com.espirit.eap.pagelayout.ListLayout;
import com.espirit.eap.pagelayout.Page;
import com.espirit.eap.pagelayout.TaglibImport;
import com.espirit.eap.util.Functions;
import com.espirit.eap.util.TaglibManager;
import com.espirit.eap.util.TxFileService;
import com.espirit.eap.util.tagAdaptor.TagAdaptorUtil;
import com.espirit.eap.util.tagAdaptor.TagElementAdaptor;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.oosearch.Condition;
import com.googlecode.cswish.struts.Action;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.conversion.PageConverter;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.StringUtil;
import com.googlecode.cswish.util.TagUtil;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.config.ConfigurationManager;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.config.entities.ResultConfig;

/**
 * Load the configuration from db
 * 
 * Model -- Button -- Layout
 * Layout -- BlockSection -- SectionElement -- reference the detail element or reference another element
 * 
 * </pre>
 * 
 * @author feng Date: 2012-09-01
 */
@Service
public class PageConverterService implements PageConverter {

	private static final Log logger = LogFactory.getLog(PageConverterService.class);
	
	
	private boolean disableCache = false;
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private GenericService genericService;
	
	@Resource
	private ElementService elementService;
	
	@Resource
	private Config config;
	
	@Resource
	private Routing routing;
	
	@Resource
	private Functions functions;
	
	@Resource
	private TagUtil tagUtil;
	
	@Resource
	private TaglibManager tagManager;
	
	@Resource
	private TxFileService txFileService;
	
	@Resource
	private TagAdaptorUtil tagAdaptorUtil;
	
	private ConfigurationManager configurationManager;
	
	// getKey() --> true/false 
	private Map<String, Boolean> converted;
	private Map<String, Boolean> invalidConverted;
	
	// NOTICE: the path is the servlet request path, index.execute.html is different with index.html, but their template is same
	// Path --> the template path
	private Map<String, String> pageTemplateMapping = new ConcurrentHashMap<String, String>();
	
	private boolean generateDebugInfo;
	
	private long startupTime = System.currentTimeMillis();
	
	@Resource
	private CacheService cacheService;
	
	private PageConverterService pageConverterService;
	
	public PageConverterService getThis() {
		if (pageConverterService == null) {
			pageConverterService = ObjectFactory.getInstance().getBean(PageConverterService.class, false);
		}
		return pageConverterService;
	}
	
	@PostConstruct
	private void init() {
		converted = cacheService.newCache(Boolean.class);
		invalidConverted = cacheService.newCache(Boolean.class, 5000);
		pageTemplateMapping = cacheService.newCache(String.class);
	}
	
	private String getKey(String className, String methodName) {
		return '#' + className + '#' + methodName;
	}
	
	@Override
	public void convertPageByClass(String simpleModelName, String methodName, PageElements ps) {
		if (methodName == null) {
			return ;
		}

		// Each method has a page template
		String key = getKey(simpleModelName, methodName);
		// Try to load it from the user cache
		boolean ret = !disableCache && (converted.get(key) != null || invalidConverted.get(key) != null);
		if (ret) {
			return ;
		}

		getThis().loadAndConvertPage(key, simpleModelName, methodName, ps);
	}
	
	@Transactional
	public void loadAndConvertPage(String key, String simpleModelName, String methodName, PageElements ps) {
		Invoker invoker = new Invoker(simpleModelName, '.', methodName, ContextHolder.get().getSiteName());
		// get layout and convert layout
		Page page = getPage(invoker);
		String realFilePath = config.getRealSitePath() + config.getViewDir4GenCode()
				+ routing.invoker2PathNoExtension(invoker, false) + ".ftl";
		
		if (page != null) {
			Layout layout = page.getLayout();
			if (layout == null) {
				if (!new File(realFilePath).exists()) {
					invalidConverted.put(key, Boolean.FALSE);
				} else {
					converted.put(key, Boolean.TRUE);
				}
			} else {
				boolean updateFileByDbRecord;
				File file = new File(realFilePath);
				if (file.exists()) {
					// compare the modify time: db record vs template
					if (layout.getModifiedOn() != null) {
						long gap = layout.getModifiedOn().getTime() - file.lastModified();
						updateFileByDbRecord = gap > 0;
					} else {
						updateFileByDbRecord = false;
					}
				} else {
					updateFileByDbRecord = true;
				}
				if (updateFileByDbRecord) {		// load the data from db
					if (logger.isDebugEnabled()) {
						logger.debug("load page definition '" + page.getPath() + "' from database");
					}
					String data = convertPage(page, false, false, ps);
					// generate file by the new data
					saveTemplate(realFilePath, data);
					// ensure the file has the same modified time with layout record
					if (layout.getModifiedOn() != null) {
						file.setLastModified(layout.getModifiedOn().getTime());
					}
				}
				converted.put(key, Boolean.TRUE);
			}
		} else {
			if (!new File(realFilePath).exists()) {
				invalidConverted.put(key, Boolean.FALSE);
			} else {
				// create the page record automatically
				Page newPage = new Page();
				String path = '/' + routing.invoker2PathNoExtension(invoker, false)
						+ '.' + functions.getActionExtension();
				newPage.setPath(path);
				newPage.setFreeAccess(Boolean.FALSE);
				newPage.setTemplate(Boolean.FALSE);
				newPage.setMethod(invoker.methodName);
				genericService.merge(newPage);
				converted.put(key, Boolean.TRUE);
			}
		}
	}
	
	/* 
	// Refer to StrutsFreemarkerManager.findRealPath
	private String findRealPath(String simpleName, char separator, String methodName) {
		String path = null;
		boolean found = false;
		String pathPrefix = config.getRealSitePath() + config.getViewDir4GenCode() 
				+ Routing.invoker2PathNoExtension(simpleName, separator, methodName, false) + '.';
		String[] suffixs = new String[] {"ftl", "html", "jsp"};
		for (String suffix : suffixs) {
			path = pathPrefix + suffix;
			
			if (new File(path).exists()) {
				found = true;
				break;
			}
		}
		
		// 2. skip the method and search again
		if (!found) {
			pathPrefix = config.getRealSitePath() + config.getViewDir4GenCode()
					+ simpleName.replace('.', '/') + '.';
			
			for (String suffix : suffixs) {
				path = pathPrefix + suffix;
				
				if (new File(path).exists()) {
					found = true;
					break;
				}
			}
		}
		return found ? path : null;
	}
	 */

	/**
	 * Convert layout definition
	 * 
	 * Page type: 
	 * 1) normal page without template
	 * 2) norml page with template
	 * 3) template page
	 * 4) top page (with top flag)
	 * 
	 * Rule 1: wrapper the form tag for the normal page
	 * Rule 2: wrapper the header/footer tag for the template page, normal page withouth template
	 * Rule 3: add js to limit the css scope
	 * 
	 * @param page
	 * @param isPageDefine the conversion is for the self-definition page (page editor)
	 */
	public String convertPage(Page page, boolean isPageDefine, boolean isDebug, PageElements ps) {
		Layout layout = page.getLayout();
		if (layout == null) {
			logger.debug("Invalid layout for page: " + page.getPath());
			return "";
		}
		
		boolean isTemplate = page.getTemplate() != null && page.getTemplate();
		boolean isFreePath = page.getFreeAccess() != null && page.getFreeAccess();
		Invoker invoker = routing.path2Invoker(page.getPath(), false);
		boolean isTopPage = page.getTopPage() != null && page.getTopPage();
		boolean wrappFormAuto = !isTopPage && !isTemplate;
		// the pageTemplate is invalid in the page definition
		//boolean wrappHeaderFooterTag = page.getPageTemplate() == null || isPageDefine;
		
		Map<String, Object> context = new HashMap<String, Object>();
		context.put(TagElementAdaptor.INVOKER, invoker);
		String formId = tagUtil.buildCidName("form", invoker.actionName);
		context.put(TagElementAdaptor.FORM_ID, formId);
		
		StringBuilder sb;
		Set<String> tagCategories;
		ConversionContext conversionContext = new ConversionContext(elementService, tagManager, tagAdaptorUtil, txFileService,
				context, generateDebugInfo, page, isPageDefine, isDebug, ps);
		
		// prepare some precondition
		PrePageConverterCallback preCallback = new PrePageConverterCallback(conversionContext);
		elementService.iterator(layout.getSortChildren(), preCallback,  wrappFormAuto ? 1 : 0);
		
		// begin conversion
		ConversionCallback callback = new ConversionCallback(conversionContext);
		elementService.iterator(layout.getSortChildren(), callback,  wrappFormAuto ? 1 : 0);
		
		sb = conversionContext.getLayoutContent();
		tagCategories = conversionContext.getTagCategories();
		
		//TODO: save the ps to PageElementService?
		PagePosition pagePosition = conversionContext.fastParse();
		
		StringBuilder tpl = new StringBuilder();
		if (isTopPage && !StringUtil.startsWith(sb, "<#assign _topPage=true>")) {
			tpl.append("<#assign _topPage=true>");
		}
		
		// 2. add the generic tag & form information
		String tagFolder = tagManager.getTagFolder();
		// all the page must include the core.ftl
		String coreTag = "<#include '" + tagFolder + "core.ftl'>";
		if (sb.indexOf(coreTag) == -1) {
			tpl.append(coreTag).append("\n");
		}
		tagCategories.remove("core");
		
		for (String category : new ArrayList<>(tagCategories)) {
			for (String dependence : tagManager.getTagCategory(category).getDependenceCategories()) {
				tagCategories.add(dependence);
			}
		}
		
		// append the custom tag
		if (layout.getTaglibImports() != null) {
			for (TaglibImport taglibImport : layout.getTaglibImports()) {
				if ("core".equals(taglibImport.getCategory()) || taglibImport.getTagCategory() == null) {
					continue;
				}
				
				String tag;
				if (taglibImport.getUseImport() != null && taglibImport.getUseImport()) {
					tag = "<#import '" + taglibImport.getTagCategory().getPath() + "' as " + taglibImport.getCategory() + ">";
				} else {
					tag = "<#include '" + taglibImport.getTagCategory().getPath() + "'>";
				}
				tpl.append(tag).append("\n");
				tagCategories.remove(taglibImport.getCategory());
			}
		}
		for (String category : tagCategories) {
			String tag = "<#include '" + tagFolder + category + ".ftl'>";
			if (sb.indexOf(tag) == -1) {
				tpl.append(tag).append("\n");
			}
		}

		// header
		// header - 1: html header
		TagPosition bodyTag = pagePosition.bodyTag;
		if (bodyTag.found()) {
			tpl.append(sb.substring(0, bodyTag.openTagPos));
		}

		// body
		// body - 1: open form tag
		if (wrappFormAuto && pagePosition.formTag.openTagPos != -1) {
			wrappFormAuto = false;
		}
		if (wrappFormAuto) {
			String modeName = beanIntrospector.getModelName(beanIntrospector.getSimpleName(invoker.actionName));
			String fromAction = functions.linkModelNoBase(modeName, invoker.methodName);
			String targetAction = functions.linkModelNoBase(modeName, invoker.getSubmitMethod());
			if (isFreePath) {
				fromAction = config.getSecurityFreePath() + fromAction.substring(1);
				targetAction = config.getSecurityFreePath() + targetAction.substring(1);
			}
			
			tpl.append("<@form").append(" name='").append(formId).append("' action='").append(targetAction)
					.append("' from='").append(fromAction).append('\'');
			String clazz = page.getClazz();
			if (conversionContext != null) {
				String categoryClass = conversionContext.getCategoryClass(page);
				if (categoryClass != null) {
					clazz = conversionContext.mergeClass(clazz, categoryClass);
				}
			}
			if (clazz != null) {
				tpl.append(" clazz='").append(clazz).append('\'');
			}
			if (isTopPage) {
				tpl.append(" includeHf=false");
			}
			boolean validateForm = tagCategories.contains("common");		// TODO: remove this logic?
			if (!validateForm) {
				tpl.append(" validator=false");
			}
			tpl.append(">\n");
		}
		
		// body - 2: real body
		if (bodyTag.found()) {
			tpl.append(sb.substring(bodyTag.openTagPos, bodyTag.closeTagPos));
		} else {
			tpl.append(sb);
		}
		
		// body - 3: close form tag
		if (wrappFormAuto) {
			tpl.append("</@form>\n");
		}
		
		// footer - 2: html footer
		if (bodyTag.found()) {
			tpl.append(sb.substring(bodyTag.closeTagPos));
		}
		
		// use js to resolve the css scope
		if (conversionContext != null && !conversionContext.getExCategoryClass().isEmpty()) {
			tpl.append("<script>");
			for (Entry<Element, String> entry : conversionContext.getExCategoryClass().entrySet()) {
				String name = tagAdaptorUtil.getName(entry.getKey());
				String exClazz = entry.getValue();
				tpl.append("addCssNotFlag('" + name + "','" + exClazz + "');");
			}
			tpl.append("</script>");
		}
		return tpl.toString();
	}
	
	/**
	 * Save the FreeMarker template, see strutsFreemarkerManager.findLocation
	 * 
	 * @param invoker
	 * @param data
	 */
	private void saveTemplate(String path, String data) {		
		File file = new File(path);
		try {
			if (!data.isEmpty()) {
				boolean enableOverride = true;
				if (file.exists()) {
					// avoid the override in the windows environment
					if (file.lastModified() >= startupTime && !file.getPath().equals(path.replace('/', File.separatorChar))) {
						if (logger.isDebugEnabled()) {
							logger.debug("Skip to create file " + path + ", " + file.getPath() + " has generated");
						}
						enableOverride = false;
					}
				}
				
				if (enableOverride) {
					FileUtils.writeStringToFile(file, data, "UTF-8");
					if (logger.isDebugEnabled()) {
						logger.debug("Creat file according to PageElements: " + path);
					}
				}
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("Skip to create file " + path + ", template configuration isn't found");
				}
			}
		} catch (IOException ex) {
			logger.error("Failed to create file by template: " + path, ex);
		}
	}
	
	/**
	 * Get the layout according to the invoker or template
	 * <p>
	 * Rule:<br>
	 * template --> invoker path.layout --> model.method.layout --> model.method.defaultLayout
	 * <p>
	 * @param invoker
	 * @param template
	 * @return
	 */
	public Layout getLayout(Invoker invoker, boolean returnLastVersion) {
		Page page = getPage(invoker);
		Layout layout = null;
		if (page != null) {
			layout = page.getLayout();
		}
		return layout;
	}
	
	public Layout getTemplateLayout(String templatePath) {
		Page example = new Page();
		example.setTemplate(Boolean.TRUE);
		example.setPath(templatePath);
		Page page = genericService.searchByModel(example, Condition.SEARCH_ONE).getData(0);
		Layout layout = null;
		if (page != null) {
			layout = page.getLayout();
		}
		return layout;
	}
	
	public Page getPage(Invoker invoker) {
		// try to load the page from db
		boolean disableCustomVersion = false;
		
		// 2. from the path
		Page example = new Page();
		example.setTemplate(Boolean.FALSE);
		if (disableCustomVersion) {
			Layout exampleLayout = new Layout();
			exampleLayout.setCustomVersion(false);
			example.setLayout(exampleLayout);
		}
		String path = functions.linkModelNoBase(invoker.actionName, invoker.methodName, false);
		example.setPath(path);
		Page page = genericService.searchByModel(example, Condition.SEARCH_ONE).getData(0);
		return page;
	}
	
	private Class<? extends Layout> getLayoutClass(String method) {
		Class<? extends Layout> clazz;
		if (method.startsWith(Action.SEARCH)) {
			clazz = ListLayout.class;
		} else {
			clazz = DetailLayout.class;
		}
		return clazz;
	}

	@Override
	public void resetPage(String simpleModelName, String method) {
		String key = getKey(simpleModelName, method);
		converted.remove(key);
		invalidConverted.remove(key);
		String path = '/' + Routing.invoker2PathNoExtension(simpleModelName, '.', method, true) + '.' + functions.getActionExtension();
		pageTemplateMapping.remove(path);
		if ("execute".equals(method)) {
			// remove the path which ends with ".execute.html"
			int suffixLen = "execute".length() + 1 + functions.getActionExtension().length();
			String path2 = path.substring(0, path.length() - suffixLen) + functions.getActionExtension();
			pageTemplateMapping.remove(path2);
		}
		
		if (Action.SEARCH.equals(method)) {
			key = getKey(simpleModelName, "_search");
			converted.remove(key);
			invalidConverted.remove(key);
		} else if (Action.PRE_SEARCH.equals(method)) {
			key = getKey(simpleModelName, "search");
			converted.remove(key);
			invalidConverted.remove(key);
		}
		
		// update the struts ResultConfig
		if (configurationManager == null) {
			if (Dispatcher.getInstance() == null) {
				return ;
			}
			
			configurationManager = Dispatcher.getInstance().getConfigurationManager();
		}
		Map<String, Map<String, ActionConfig>> actionConfigs = configurationManager.getConfiguration()
				.getRuntimeConfiguration().getActionConfigs();
		// for example: espirit/eap/manager/menu._search
		String context = ContextHolder.get().getSiteName() + '/' + simpleModelName.replace('.', '/') + '.' + method;
		ActionConfig actionConfig = actionConfigs.get("").get(context);
		if (actionConfig != null) {
			Map<String, ResultConfig> results = actionConfig.getResults();
			ResultConfig resultConfig = results.get(Action.SUCCESS);
			if (resultConfig != null) {
				ConcurrentHashMap<String, ResultConfig> map;
				if (results instanceof ConcurrentHashMap) {
					map = (ConcurrentHashMap<String, ResultConfig>) results;
				} else {
					map = new ConcurrentHashMap<String, ResultConfig>(results);
					ModelUtil.setField(actionConfig, "results", map);
				}
				// remove the cache, force struts to call CSwishUnknownHandler.handleUnknownResult
				map.remove(Action.SUCCESS);
			}
		}
	}
	
	/**
	 * scan all key and reset it (include all the template of the action)
	 */
	@Override
	public void resetPage(String actionName) {
		List<String> invalidKeys = new ArrayList<String>();
		String prefix = actionName + '.';
		for (String key : new ArrayList<String>(converted.keySet())) {
			if (key.startsWith(prefix)) {
				invalidKeys.add(key);
			}
		}
		for (String key : new ArrayList<String>(invalidConverted.keySet())) {
			if (key.startsWith(prefix)) {
				invalidKeys.add(key);
			}
		}
		for (String key : invalidKeys) {
			String[] keys = StringUtils.split(key, '#');
			resetPage(keys[0], keys[1]);
		}
	}

	@Override
	public boolean hasPage(String simpleModelName, String methodName) {
		// Each method has a page template
		String key = getKey(simpleModelName, methodName);
		if (!disableCache) {
			Boolean contain = converted.get(key);
			if (contain != null) {
				return contain;
			}
			contain = invalidConverted.get(key);
			if (contain != null) {
				return contain;
			}
		}
		
		// ensure the class is generated
		convertPageByClass(simpleModelName, methodName, new PageElements());
		Boolean contain = converted.get(key);
		return contain != null && contain;
	}

	@Override
	public String getPageTemplate(String pageRequestPath) {
		String templateLayoutPath = pageTemplateMapping.get(pageRequestPath);
		
		if (templateLayoutPath == null) {
			templateLayoutPath = getThis().getTemplateLayoutPath(pageRequestPath);
		}
		return templateLayoutPath.isEmpty() ? null : config.getSitePath() + config.getViewDir4GenCode() + templateLayoutPath;
	}
	
	@Transactional
	public String getTemplateLayoutPath(String pageRequestPath) {
		String templateLayoutPath;
		
		Invoker invoker = routing.path2Invoker(pageRequestPath, true);
		String formatPath = '/' + routing.invoker2PathNoExtension(invoker, false) + '.' + functions.getActionExtension();
		
		Page example = new Page();
		example.setPath(formatPath);
		example.setTemplate(Boolean.FALSE);
		Page page = genericService.searchByModel(example, Condition.SEARCH_ONE).getData(0);
		if (page != null && page.getPageTemplate() != null) {
			templateLayoutPath = page.getPageTemplate().getPath();
			boolean valid = converted.containsKey(templateLayoutPath);
			if (!valid) {		// template id is valid
				String realPath = config.getRealSitePath() + config.getViewDir4GenCode() + templateLayoutPath;
				File file = new File(realPath);
				Layout layout = page.getPageTemplate().getLayout();
				boolean updateTplByDb = false;
				if (layout != null) {
					if (!file.exists() || layout.getModifiedOn() != null && file.lastModified() < layout.getModifiedOn().getTime()) {
						updateTplByDb = true;
					}
				}
				if (updateTplByDb) {
					String data = convertPage(page.getPageTemplate(), false, false, new PageElements());
					file.getParentFile().mkdirs();
					
					try {
						FileUtils.writeStringToFile(file, data, "UTF-8");
					} catch (IOException ex) {
						logger.error("Failed to create template file: " + pageRequestPath, ex);
					}
				}
				converted.put(templateLayoutPath, Boolean.TRUE);
			}
		} else {
			templateLayoutPath = "";
		}
		pageTemplateMapping.put(pageRequestPath, templateLayoutPath);
		
		return templateLayoutPath;
	}

	@Override
	public void resetPageTemplate(String templatePath) {
		for (Entry<String, String> pathTemplate : pageTemplateMapping.entrySet()) {
			if (pathTemplate.getValue().equals(templatePath)) {
				pageTemplateMapping.remove(pathTemplate.getKey());
			}
		}
		converted.remove(templatePath);
	}
}